/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.volley.toolbox;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.widget.ImageView.ScaleType;

import com.android.volley.DefaultRetryPolicy;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyLog;

/**
 * A canned request for getting an image at a given URL and calling
 * back with a decoded Bitmap.
 */
public class ImageRequest extends Request< Bitmap >
{
	/** Socket timeout in milliseconds for image requests */
	private static final int IMAGE_TIMEOUT_MS = 1000;
	
	/** Default number of retries for image requests */
	private static final int IMAGE_MAX_RETRIES = 2;
	
	/** Default backoff multiplier for image requests */
	private static final float IMAGE_BACKOFF_MULT = 2f;
	
	private final Response.Listener< Bitmap > mListener;
	private final Config mDecodeConfig;
	private final int mMaxWidth;
	private final int mMaxHeight;
	private ScaleType mScaleType;
	
	/** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
	private static final Object sDecodeLock = new Object();
	
	/**
	 * Creates a new image request, decoding to a maximum specified width and
	 * height. If both width and height are zero, the image will be decoded to
	 * its natural size. If one of the two is nonzero, that dimension will be
	 * clamped and the other one will be set to preserve the image's aspect
	 * ratio. If both width and height are nonzero, the image will be decoded to
	 * be fit in the rectangle of dimensions width x height while keeping its
	 * aspect ratio.
	 * @param url URL of the image
	 * @param listener Listener to receive the decoded bitmap
	 * @param maxWidth Maximum width to decode this bitmap to, or zero for none
	 * @param maxHeight Maximum height to decode this bitmap to, or zero for
	 * none
	 * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
	 * @param decodeConfig Format to decode the bitmap to
	 * @param errorListener Error listener, or null to ignore errors
	 */
	public ImageRequest(
			String url,
			Response.Listener< Bitmap > listener,
			int maxWidth,
			int maxHeight,
			ScaleType scaleType,
			Config decodeConfig,
			Response.ErrorListener errorListener )
	{
		super( Method.GET, url, errorListener );
		setRetryPolicy( new DefaultRetryPolicy( IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT ) );
		mListener = listener;
		mDecodeConfig = decodeConfig;
		mMaxWidth = maxWidth;
		mMaxHeight = maxHeight;
		mScaleType = scaleType;
	}
	
	/**
	 * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to
	 * the normal constructor with {@code ScaleType.CENTER_INSIDE}.
	 */
	@Deprecated
	public ImageRequest(
			String url,
			Response.Listener< Bitmap > listener,
			int maxWidth,
			int maxHeight,
			Config decodeConfig,
			Response.ErrorListener errorListener )
	{
		this( url, listener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE, decodeConfig, errorListener );
	}
	
	@Override
	public Priority getPriority()
	{
		return Priority.LOW;
	}
	
	/**
	 * Scales one side of a rectangle to fit aspect ratio.
	 * @param maxPrimary Maximum size of the primary dimension (i.e. width for
	 * max width), or zero to maintain aspect ratio with secondary
	 * dimension
	 * @param maxSecondary Maximum size of the secondary dimension, or zero to
	 * maintain aspect ratio with primary dimension
	 * @param actualPrimary Actual size of the primary dimension
	 * @param actualSecondary Actual size of the secondary dimension
	 * @param scaleType The ScaleType used to calculate the needed image size.
	 */
	private static int getResizedDimension(
			int maxPrimary,
			int maxSecondary,
			int actualPrimary,
			int actualSecondary,
			ScaleType scaleType )
	{
		
		// If no dominant value at all, just return the actual.
		if( ( maxPrimary == 0 ) && ( maxSecondary == 0 ) )
		{
			return actualPrimary;
		}
		
		// If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
		if( scaleType == ScaleType.FIT_XY )
		{
			if( maxPrimary == 0 )
			{
				return actualPrimary;
			}
			return maxPrimary;
		}
		
		// If primary is unspecified, scale primary to match secondary's scaling ratio.
		if( maxPrimary == 0 )
		{
			double ratio = ( double )maxSecondary / ( double )actualSecondary;
			return ( int )( actualPrimary * ratio );
		}
		
		if( maxSecondary == 0 )
		{
			return maxPrimary;
		}
		
		double ratio = ( double )actualSecondary / ( double )actualPrimary;
		int resized = maxPrimary;
		
		// If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
		if( scaleType == ScaleType.CENTER_CROP )
		{
			if( ( resized * ratio ) < maxSecondary )
			{
				resized = ( int )( maxSecondary / ratio );
			}
			return resized;
		}
		
		if( ( resized * ratio ) > maxSecondary )
		{
			resized = ( int )( maxSecondary / ratio );
		}
		return resized;
	}
	
	@Override
	protected Response< Bitmap > parseNetworkResponse( NetworkResponse response )
	{
		// Serialize all decode on a global lock to reduce concurrent heap usage.
		synchronized( sDecodeLock )
		{
			try
			{
				return doParse( response );
			}
			catch( OutOfMemoryError e )
			{
				VolleyLog.e( "Caught OOM for %d byte image, url=%s", response.data.length, getUrl() );
				return Response.error( new ParseError( e ) );
			}
		}
	}
	
	/**
	 * The real guts of parseNetworkResponse. Broken out for readability.
	 */
	private Response< Bitmap > doParse( NetworkResponse response )
	{
		byte[] data = response.data;
		BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
		Bitmap bitmap = null;
		if( mMaxWidth == 0 && mMaxHeight == 0 )
		{
			decodeOptions.inPreferredConfig = mDecodeConfig;
			bitmap = BitmapFactory.decodeByteArray( data, 0, data.length, decodeOptions );
		}
		else
		{
			// If we have to resize this image, first get the natural bounds.
			decodeOptions.inJustDecodeBounds = true;
			BitmapFactory.decodeByteArray( data, 0, data.length, decodeOptions );
			int actualWidth = decodeOptions.outWidth;
			int actualHeight = decodeOptions.outHeight;
			
			// Then compute the dimensions we would ideally like to decode to.
			int desiredWidth = getResizedDimension( mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType );
			int desiredHeight = getResizedDimension( mMaxHeight, mMaxWidth, actualHeight, actualWidth, mScaleType );
			
			// Decode to the nearest power of two scaling factor.
			decodeOptions.inJustDecodeBounds = false;
			// TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
			// decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
			decodeOptions.inSampleSize = findBestSampleSize( actualWidth, actualHeight, desiredWidth, desiredHeight );
			Bitmap tempBitmap = BitmapFactory.decodeByteArray( data, 0, data.length, decodeOptions );
			
			// If necessary, scale down to the maximal acceptable size.
			if( tempBitmap != null && ( tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight ) )
			{
				bitmap = Bitmap.createScaledBitmap( tempBitmap, desiredWidth, desiredHeight, true );
				tempBitmap.recycle();
			}
			else
			{
				bitmap = tempBitmap;
			}
		}
		
		if( bitmap == null )
		{
			return Response.error( new ParseError( response ) );
		}
		else
		{
			return Response.success( bitmap, HttpHeaderParser.parseCacheHeaders( response ) );
		}
	}
	
	@Override
	protected void deliverResponse( Bitmap response )
	{
		mListener.onResponse( response );
	}
	
	/**
	 * Returns the largest power-of-two divisor for use in downscaling a bitmap
	 * that will not result in the scaling past the desired dimensions.
	 * @param actualWidth Actual width of the bitmap
	 * @param actualHeight Actual height of the bitmap
	 * @param desiredWidth Desired width of the bitmap
	 * @param desiredHeight Desired height of the bitmap
	 */
	// Visible for testing.
	static int findBestSampleSize(
			int actualWidth,
			int actualHeight,
			int desiredWidth,
			int desiredHeight )
	{
		double wr = ( double )actualWidth / desiredWidth;
		double hr = ( double )actualHeight / desiredHeight;
		double ratio = Math.min( wr, hr );
		float n = 1.0f;
		while( ( n * 2 ) <= ratio )
		{
			n *= 2;
		}
		
		return ( int )n;
	}
}
